1. polars 소개
약 14분 읽기
1. Polars 소개
1.1 Polars란 무엇인가
Polars(폴라스) 는 Rust로 작성된 초고속 데이터프레임 라이브러리로, 구조화된 데이터(structured data)를 효율적으로 조작하고 분석하기 위한 도구입니다. Python의 Pandas, R의 data.frame, Apache Spark의 DataFrame과 같은 개념적 계보에 서 있지만, 처음부터 성능과 메모리 효율성을 최우선으로 설계된 점이 가장 큰 차이점입니다.
Polars 공식 문서는 Polars를 다음과 같이 정의합니다:
“Polars is a blazingly fast DataFrame library for manipulating structured data. The core is written in Rust, and available for Python, R and NodeJS.”
한국어로 옮기면 다음과 같습니다:
“Polars는 구조화된 데이터를 다루기 위한 눈부시게 빠른 데이터프레임 라이브러리입니다. 핵심은 Rust로 작성되었으며, Python, R, NodeJS에서 사용할 수 있습니다.”
즉, Polars의 핵심 엔진(query engine) 은 Rust로 작성되어 있고, 그 위에 Python, R, Node.js 등 다양한 언어 바인딩이 얹혀 있는 구조입니다. 이 책에서는 이 핵심 엔진 자체를 Rust에서 직접 다루는 방법을 집중적으로 살펴봅니다.
데이터프레임이란?
데이터프레임(DataFrame)은 2차원 표 형태의 데이터 구조로, 행(row)과 열(column)로 이루어져 있습니다. 각 열은 이름을 가지며, 동일한 데이터 타입을 갖는 값들의 모임입니다. 예를 들어 다음과 같은 표를 생각해 봅시다.
| name | age | city |
|---|---|---|
| Alice | 30 | Seoul |
| Bob | 25 | Busan |
| Charlie | 35 | Incheon |
이 표는 세 개의 열(name, age, city)과 세 개의 행으로 구성된 데이터프레임입니다. name 열은 문자열, age 열은 정수, city 열은 문자열 타입을 가집니다. 데이터프레임은 이처럼 이질적인 타입의 열들을 하나의 표로 묶어 다룰 수 있게 해주는 자료구조이며, 데이터 분석·ETL·머신러닝 전처리 등 거의 모든 데이터 작업의 출발점이 됩니다.
Polars의 위치
Polars는 단순한 데이터프레임 라이브러리를 넘어, OLAP(Online Analytical Processing) 쿼리 엔진의 성격을 강하게 띱니다. 즉, 트랜잭션 처리(OLTP)보다는 대용량 데이터에 대한 읽기 중심의 분석 질의를 빠르게 처리하는 데 최적화되어 있습니다. 공식 GitHub 저장소는 Polars를 다음과 같이 소개합니다:
“Polars is an analytical query engine written for DataFrames.”
이러한 성격 때문에 Polars는 단일 머신에서 작동하는 도구임에도 불구하고, 분산 시스템인 Spark나 DuckDB와 자주 비교되며, 많은 벤치마크에서 단일 머신 기준으로 압도적인 성능을 보여줍니다.
1.2 왜 Polars를 사용해야 하는가 (Pandas와의 비교)
데이터 분석 분야에서 Pandas는 사실상 표준(de facto standard)으로 자리 잡아 왔습니다. 그렇다면 왜 굳이 Polars라는 새로운 도구를 배워야 할까요? 이 질문에 답하기 위해 가장 널리 알려진 비교 대상인 Pandas와 Polars를 여러 차원에서 비교해 보겠습니다.
1.2.1 성능 비교
Pandas는 2008년에 Wes McKinney에 의해 처음 개발되었으며, NumPy 위에 구축된 단일 스레드 기반의 라이브러리입니다. 반면 Polars는 2020년에 Richie Vink에 의해 시작된 프로젝트로, 처음부터 Rust와 Apache Arrow를 기반으로 다중 스레드 병렬 처리를 염두에 두고 설계되었습니다.
일반적으로 다음과 같은 작업에서 Polars가 Pandas보다 수 배에서 수십 배 빠릅니다:
- 대용량 CSV/Parquet 파일 읽기
- 그룹화(group by)와 집계(aggregation)
- 조인(join) 연산
- 필터링(filter)이 포함된 복잡한 파이프라인
예를 들어 5GB 크기의 CSV 파일을 읽어 그룹화와 집계를 수행하는 작업에서 Pandas가 30초가 걸린다면, Polars는 동일 작업을 3~5초 안에 끝내는 경우가 흔합니다. 이러한 차이는 데이터가 커질수록 더 벌어집니다.
1.2.2 메모리 효율성
Pandas는 내부적으로 NumPy 배열을 사용하며, 특히 문자열 데이터를 처리할 때 상당한 메모리 오버헤드를 가집니다. 또한 연산 과정에서 중간 데이터 복사가 빈번하게 발생하여, 큰 데이터를 다룰 때 메모리 부족(Out-Of-Memory, OOM) 현상을 겪기 쉽습니다.
반면 Polars는 Apache Arrow의 컬럼 기반(columnar) 메모리 포맷을 사용하여 메모리 사용량이 획기적으로 적습니다. 또한 지연 평가(lazy evaluation) 와 스트리밍 엔진(streaming engine) 을 통해 메모리보다 큰 데이터도 처리할 수 있습니다.
1.2.3 API 설계 철학
Pandas는 오랜 역사와 방대한 사용자층을 가진 만큼 API가 매우 풍부하지만, 그만큼 일관성이 떨어지는 부분도 있습니다. 같은 작업을 여러 방식으로 할 수 있고, 인덱스(index)라는 개념이 혼란을 일으키는 경우도 많습니다.
Polars는 처음부터 표현식 기반(expression-based) API 로 설계되었습니다. 모든 변환은 명확한 표현식으로 표현되며, 일관된 방식으로 합성(compose)할 수 있습니다. 또한 인덱스라는 개념을 과감히 제거하여, 사용자가 데이터 자체에 집중할 수 있도록 했습니다.
1.2.4 비교 요약 표
| 항목 | Pandas | Polars |
|---|---|---|
| 구현 언어 | Python/C(Cython) | Rust |
| 메모리 포맷 | NumPy 배열 | Apache Arrow |
| 스레딩 | 단일 스레드(주로) | 다중 스레드(기본) |
| 평가 방식 | 즉시 평가(eager) | 지연 평가(lazy) + 즉시 평가 |
| 인덱스 | 있음(혼란의 원인) | 없음 |
| 쿼리 최적화 | 없음 | 있음(푸시다운, 프로젝션 등) |
| 스트리밍 | 부분 지원 | 완전 지원 |
| 메모리 초과 데이터 | 제한적 | 스트리밍으로 처리 가능 |
| 학습 곡선 | 완만(역사가 길어) | 다소 가파름(표현식 개념 필요) |
물론 Pandas에게도 장점은 있습니다. 생태계가 훨씬 오래되었기에, 통계 모델링, 시각화, 특정 도메인 라이브러리와의 통합 등에서는 여전히 Pandas가 유리한 영역이 있습니다. 그러나 순수한 데이터 처리 속도와 효율성이 중요한 작업에서는 Polars가 훨씬 유리합니다.
1.3 주요 특징
Polars의 핵심 특징을 더 깊이 살펴보겠습니다.
1.3.1 속도 (Speed)
Polars가 빠른 이유는 단일한 기술 때문이 아니라 여러 요인의 결합입니다:
- Rust의 성능: Rust는 C/C++에 필적하는 성능을 내면서도 메모리 안전성을 보장하는 언어입니다. 가비지 컬렉터가 없어 런타임 오버헤드가 적습니다.
- Apache Arrow 포맷: 컬럼 기반 메모리 레이아웃은 CPU 캐시 친화적이며, 벡터화된 SIMD 연산에 유리합니다.
- 다중 스레드 병렬 처리: 데이터를 청크(chunk) 단위로 나누어 여러 CPU 코어에서 동시에 처리합니다.
- 쿼리 최적화기: 지연 평가를 통해 전체 쿼리를 분석한 뒤, 가장 효율적인 실행 계획을 수립합니다.
1.3.2 메모리 효율성 (Memory Efficiency)
Polars는 다음과 같은 방식으로 메모리를 절약합니다:
- 제로 카피(Zero-copy): Apache Arrow의 불변(immutable) 메모리를 사용하여, 데이터 복사 없이 뷰(view)를 만들 수 있습니다.
- 문자열 인코딩: 문자열은 UTF-8로 저장되며, 반복되는 문자열은 자동으로 카테고리형(categorical)으로 압축될 수 있습니다.
- 스트리밍: 메모리에 한 번에 올리지 않고 배치(batch) 단위로 처리합니다.
- 푸시다운 최적화: 필요한 열과 행만 데이터 소스에서 읽어옵니다(projection/predicate pushdown).
1.3.3 지연 평가 (Lazy Evaluation)
Polars의 가장 강력한 특징 중 하나입니다. 즉시 평가(eager evaluation) 방식에서는 각 연산이 호출되는 즉시 실행되지만, 지연 평가에서는 연산이 쿼리 계획(query plan)으로 축적되었다가, collect() 같은 호출이 있을 때 한꺼번에 실행됩니다.
예를 들어 다음 파이프라인을 생각해 봅시다:
- CSV 파일 읽기
- 특정 열 선택
- 조건에 따라 행 필터링
- 그룹화와 집계
즉시 평가 방식에서는 1번 과정에서 파일 전체를 메모리에 올립니다. 그러나 지연 평가에서는 4번 단계까지 전체 계획을 본 뒤, “결국 필요한 열은 몇 개뿐이고, 조건에 맞는 행만 읽으면 되겠다“라고 판단하여, 데이터 소스 단계에서부터 효율적으로 읽어들입니다. 이를 프로젝션 푸시다운(projection pushdown), 프레디케이트 푸시다운(predicate pushdown) 이라 부릅니다.
1.3.4 다중 스레드 (Multi-threading)
Polars는 데이터를 여러 청크로 나누고, 각 청크를 서로 다른 스레드에서 동시에 처리합니다. 사용자가 명시적으로 스레드를 관리할 필요 없이, Polars가 사용 가능한 CPU 코어를 자동으로 활용합니다. Rayon이라는 Rust 데이터 병렬성 라이브러리를 사용하여, 반복문과 변환 작업을 자동으로 병렬화합니다.
1.4 아키텍처 개요 (Apache Arrow 기반)
Polars의 아키텍처를 이해하려면 Apache Arrow가 무엇인지 먼저 알아야 합니다.
1.4.1 Apache Arrow란?
Apache Arrow는 메모리 내 분석을 위한 컬럼형(columnar) 메모리 포맷을 정의하는 오픈 소스 프로젝트입니다. 데이터베이스, 데이터프레임 라이브러리, 빅데이터 시스템들이 서로 데이터를 복사 없이 주고받을 수 있게 해주는 일종의 표준 규격입니다.
Arrow의 핵심 아이디어:
- 컬럼형 레이아웃: 같은 타입의 값들이 메모리에 연속적으로 배치되어, CPU 캐시 효율과 SIMD 명령 활용이 좋습니다.
- 언어 독립적: C, C++, Rust, Python, Java 등 다양한 언어에서 동일한 메모리 포맷을 사용하므로, 언어 간 데이터 교환 시 복사나 직렬화 비용이 발생하지 않습니다(제로 카피).
- 불변(immutable) 메모리: 한 번 만들어진 Arrow 배열은 변경되지 않아, 여러 읽기 작업이 안전하게 동시에 이루어질 수 있습니다.
1.4.2 Polars의 계층 구조
Polars는 내부적으로 여러 서브 크레이트(sub-crate)로 구성되어 있습니다:
| 크레이트 | 역할 |
|---|---|
polars-core | 핵심 데이터 구조(DataFrame, Series 등) |
polars-lazy | 지연 평가 엔진과 쿼리 최적화기 |
polars-io | CSV, Parquet, JSON 등 입출력 |
polars-ops | 다양한 연산(filter, join, group_by 등) |
polars-arrow | Apache Arrow 관련 유틸리티 |
polars-plan | 쿼리 계획 표현과 최적화 |
polars-pipe | 스트리밍 실행 엔진 |
polars | 위 모든 것을 묶은 상위 크레이트(사용자용) |
사용자는 보통 최상위 polars 크레이트만 의존성에 추가하면 되지만, 필요에 따라 개별 서브 크레이트를 사용할 수도 있습니다.
1.4.3 ChunkedArray
Polars의 가장 하위 데이터 구조는 ChunkedArray<T>입니다. 이는 Arrow 배열을 여러 개의 “청크(chunk)“로 감싼 형태입니다. 왜 청크로 나누는 걸까요?
- 불변성 유지: Arrow 메모리는 불변이므로, 데이터를 추가하려면 새 배열을 만들어 이어 붙여야 합니다. 청크 구조는 이를 자연스럽게 표현합니다.
- 점진적 읽기: 큰 파일을 읽을 때, 이미 읽은 부분과 앞으로 읽을 부분을 청크로 관리하여 메모리를 절약합니다.
- 병렬 처리: 청크별로 서로 다른 스레드가 작업할 수 있어 병렬성이 자연스럽습니다.
ChunkedArray<T>에서 T는 원소 타입(예: Int32Type, Utf8Type 등)을 나타냅니다. 이 위에 Series(동적 타입 래퍼), DataFrame(Series들의 모임) 순서로 추상화가 쌓입니다. 이 구조는 2부에서 더 자세히 다룹니다.